require 'abstractvirtualnode'
require 'remotevirtualnode'
require 'listenervirtualnode'
require 'thread'

# Implementacao de AbstractVirtualNode local. Representa um virtual node
# local do Chord.
class LocalVirtualNode < AbstractVirtualNode  
  attr_reader :finger_table 
  attr_accessor :host
  STABILIZE_THREAD_DELAY = 3
  FIX_FINGERS_THREAD_DELAY = 3
  CHECK_PREDECESSORS_THREAD_DELAY = 3

  # Construtor
  # * +id+ - Instancia de Key, representa chave unica desse virtual node.
  # * +host+ - Host sobre o qual esta rodando.
  # * +port+ - Porta de esculta.
  # * +num_successors+ - Numero de sucessores a serem mantidos.
  # * +callback+ - Metodo a ser chamado quando a lista de sucessores mudar, seja
  #                devido a entrada ou saida de no's.
  def initialize(id, host, port, num_successors, callback, pred_callback)
    @node_id = id
    @port = port
    @host = host
    @listener_node = ListenerVirtualNode.new(self)
    @server = DRb::DRbServer.new("druby://#{host}:#{port}", @listener_node)
    @threads = []
    @sucessors = []
    @num_successors = num_successors
    @predecessor = nil
    @finger_table = []
    @alive = true
    @next = 0
    @callback = callback
    @pred_callback = pred_callback
    @update_lock = Mutex.new
    @old_succs = []
  end

  # Implementacao efetiva do find_sucessor.
  #
  # Se da em duas etapas:
  # 1. Checa atraves da lista de sucessores.
  # 2. Acha o no mais proximo atraves da finger table e chama recursivamente.
  def find_successor(id)
    # Checa proximos
    @successors.each do |successor|
      if id.is_in_range(@node_id, successor.node_id, false)
        return successor
      end
    end
    begin    
      get_closest_preceding_node(id).find_successor(id)
    rescue Exception => e
      #puts "find -> get closest"
      puts "#{e.class} :#{e.message}"
    end
  end
    
  # Chamado quando um no' entra no Chord ring e deseja notificar seu novo
  # sucessor da sua presenca.
  # Notify soh eh chamado pelo pred com os succs ja atualizados.
  def notify(node)
    if (@predecessor == nil or (@predecessor.ping and node.node_id.is_in_range(@predecessor.node_id, @node_id, false, false)))
      @predecessor = node
      @pred_callback.call(@node_id.to_s, node.node_id.to_s, node.host)
      #@predecessor.update_succ_list(@successors)
    end
  end

  # O primeiro virtual node a ser instanciado precisa ter esse metodo chamado pois
  # ele sera o ponto de partida para entrada dos outros no's.
  def create
    @predecessor = nil
    #TODO Setar esse protocol como constante depois
    #Tem que criar um remoto pois sera passado pela get_successors
    @successors = [RemoteVirtualNode.new('druby', @host, @port)]
    start_stabilize_threads
  end
  
  # Entra no Chord ring atraves do no' passado.
  def join(node)
    @predecessor = nil
    @successors = [node.find_successor(@node_id)]
    @successors.concat(@successors[0].get_succ_list())
    start_stabilize_threads
  end

  # Retorna a lista de sucessores.
  def get_succ_list
    @successors
  end

  # Retorna a lista de predecessores.
  def get_pred_list
    @predecessor
  end
  
  def get_item(key)
    #TODO Implementar.
  end
  
  def insert_item(key, value)
    #TODO Implementar.
  end
  
  def remove_item(key)
    #TODO Implementar.
  end

  # Estabiliza o a lista de sucessores.
  # Checa se os sucessores que estao vivos e determina se o sucessor imediato esta
  # correto.
  def stabilize
    
    begin
      succs = []
      
      notify = false
      @successors.each do |node|
        begin
          if node != nil and node.ping
            succs << node
          else
            notify = true  
          end 
        rescue Exception => e
          notify = true
          #puts "NAO PINGOU"
          # por algum motivo o ping ta lancando uma excecao
          # e quando eu coloco esse begin rescue funciona mesmo lancando a excecao
        end
      end
      
      
      @successors = (succs != [] ? succs : [RemoteVirtualNode.new('druby', @host, @port)])
      
      node = @successors.first.get_pred_list
    
      if node
        if node.node_id.is_in_range(@node_id, @successors.first.node_id, false, false)
          @successors = [node]
          aux_successors = @successors.first.get_succ_list()
          update_succ_list(aux_successors, true)
          
        elsif notify
          
          succs_up = []
          @successors.each do |node|
            succs_up << [node.node_id.key.to_s, node.host]
          end
          
          @callback.call(@node_id.key.to_s, succs_up)
          
        end
      elsif notify
        
        succs_up = []
        @successors.each do |node|
          succs_up << [node.node_id.key.to_s, node.host]
        end
        
        @callback.call(@node_id.key.to_s, succs_up)
      end
      
      @predecessor.update_succ_list(@successors, false) if @predecessor
      @successors.first.notify(self)
      
      @successors.each{|s| @old_succs << s.node_id.key.to_s}
      
      
    rescue Exception => e
      #puts "STABILIZE : #{e.class} : #{e.message}"
      #puts "Stabilize -> notify"
      #puts "#{e.class} :#{e.message}"
      #puts @node_id
      #puts "------------------------------------------------"
    end
  end

  # Atualiza a lista de sucessores com base na lista passada.
  def update_succ_list(successors, notify)
    begin
      @update_lock.lock
    successors = successors[0...(@num_successors-1)] if successors.length >= @num_successors
    
    old_succs = []
    @successors.each{|s| old_succs << s.node_id.key.to_s}
    
    # node_ids_curr = []
    # @successors.each do |s|
      # node_ids_curr << s.node_id.key
    # end
    
    @successors =  [@successors.first]
    @successors.concat(successors)
    compact_succs
    
    changed = false
    if old_succs.length != @successors.length
      changed = true
    else
      i = 0
      while i < old_succs.length 
        if old_succs[i] != @successors[i].node_id.key.to_s
          changed = true
          break
        end
        i += 1
      end
    end
    
    if changed || notify
      @predecessor.update_succ_list(@successors, false) if @predecessor
      
      succs_up = []
      @successors.each do |node|
        succs_up << [node.node_id.key.to_s, node.host]
      end
      
      @callback.call(@node_id.key.to_s, succs_up)
    end
    rescue Exception => e
      #puts e.backtrace.join("\n")
      #puts "UPDATESUCCSLIST : #{e.class} : #{e.message}"
      
    ensure
      @update_lock.unlock
    end
    
    # @successors =  [@successors.first]
    # successors = successors[0...(@num_successors-1)] if successors.length >= @num_successors
    # @successors.concat(successors)
    # compact_succs
    # @callback.call
  end
  
  def compact_succs
    begin
      succs = []
      @successors.compact!
      @successors.delete_if{|succ| !succ.ping}
      @successors.uniq!
      @successors.each do |succ|
        succs << succ if !succ.node_id.eql?(@node_id)
      end
      @successors = succs
    rescue Exception => e
      #puts "COMPACTSUCCS : #{e.class} : #{e.message}"
    end
  end

  # Atualiza a entrada na finger table de acordo com o valor de next.
  def fix_fingers
    @next = @next + 1 > 10 ? 1 : @next + 1
    @finger_table[@next - 1] = self.find_successor(@node_id.+(2**(@next - 1)))
  end

  # Checa o predecessor.
  # Se ele nao responder o predecessor atual e descartado.
  def check_predecessors
    begin
      if @predecessor != nil and !@predecessor.ping
        @predecessor = nil
        @pred_callback.call(@node_id.to_s, "")
      end
    rescue Exception => e
      @predecessor = nil
      @pred_callback.call(@node_id.to_s, "")
    end
  end

  # Deixa o Chord ring voluntariamente.
  def leave
    @alive = false
    @server.stop_service()
    @threads.each{|thread| thread.join}
  end

  # Retorna se ainda esta vivo.
  def ping
    return @alive
  end
  
private
  # Cria as threads de stabilizacao necessarias.
  # Sao 3 threads:
  # 1. Thread que roda o metodo stabilize periodicamente.
  # 2. Thread que roda o metodo fix_fingers periodicamente.
  # 2. Thread que roda o metodo check_predecessors periodicamente.
  def start_stabilize_threads
    @threads << Thread.new do
      while @alive
        begin
          stabilize
          sleep(STABILIZE_THREAD_DELAY)
        rescue Exception => e
          #puts "STABILIZE EXCEPTION"
          #puts "#{e.class} : #{e.message}"
        end
      end
    end
    @threads << Thread.new do
      while @alive
        begin
          fix_fingers
          sleep(FIX_FINGERS_THREAD_DELAY)
        rescue Exception => e
          #puts "FIX FINGERS EXCEPTION"
          #puts "#{e.class} : #{e.message}"
        end
      end
    end
    @threads << Thread.new do
      while @alive
        begin
          check_predecessors
          sleep(CHECK_PREDECESSORS_THREAD_DELAY)
        rescue Exception => e
          #puts "CHECK PREDECESSORS EXCEPTION"
          #puts "#{e.class} : #{e.message}"
        end
      end
    end    
  end

  # Consulta a finger table para determinar o no' mais proximo de acordo com id
  # passado.
  def get_closest_preceding_node(id)
    @finger_table.reverse.each do |remote_node|
      if remote_node != nil and remote_node.ping and remote_node.node_id.is_in_range(@node_id, id)
        return remote_node
      end
    end
    return self
  end
end
